﻿using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO;
using System.Threading;

namespace OpenWaves.ImageTransformations.Web
{
    public class ConcurrentFileStore : IConcurrentFileStore
    {
        private readonly IFileStore fileStore;
        private readonly ConcurrentDictionary<string, ManualResetEventSlim> fileLocks = new ConcurrentDictionary<string, ManualResetEventSlim>(StringComparer.OrdinalIgnoreCase);

        public ConcurrentFileStore(IFileStore fileStore)
        {
            Contract.Requires<ArgumentNullException>(fileStore != null);
            this.fileStore = fileStore;
        }

        public Url GetOrCreateFileUrl(string fileName, TimeSpan waitTimeout, Action<Stream> fileCreatedCallback)
        {
            if (this.fileStore.FileExists(fileName))
            {
                //
                // File exists, but it might still be writen to by another thread
                //
                this.WaitForWriter(fileName, waitTimeout);
            }
            else if (this.EnterWriterLock(fileName) == false)
            {
                //
                // Failed to acquire lock. Another thread has created the file and might be writing to it.
                //
                this.WaitForWriter(fileName, waitTimeout);
            }
            else
            {
                try
                {
                    if (this.fileStore.FileExists(fileName))
                    {
                        //
                        // Another thread must have created the file after we checked and finished writing
                        // before we acquired the lock
                        //
                        this.ExitWriterLock(fileName);
                    }
                }
                catch
                {
                    this.ExitWriterLock(fileName);
                    throw;
                }

                try
                {
                    this.CreateFile(fileName, fileCreatedCallback);
                }
                finally 
                {
                    this.ExitWriterLock(fileName);
                }
            }

            return this.fileStore.GetFileUrl(fileName);
        }

        private void CreateFile(string fileName, Action<Stream> fileCreatedCallback)
        {
            try
            {
                using (var stream = this.fileStore.CreateFile(fileName))
                {
                    fileCreatedCallback(stream);
                }
            }
            catch (Exception)
            {
                this.DeleteFile(fileName);
                throw;
            }
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void DeleteFile(string fileName)
        {
            try
            {
                this.fileStore.DeleteFile(fileName);
            }
            catch (Exception deletionException)
            {
                Log.Warning(deletionException, "Failed to delete '{0}' when aborting file creation.", fileName);
            }
        }

        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        private bool EnterWriterLock(string fileName)
        {
            return this.fileLocks.TryAdd(fileName, new ManualResetEventSlim());
        }

        [SuppressMessage("Microsoft.Usage", "CA2219:DoNotRaiseExceptionsInExceptionClauses")]
        private void WaitForWriter(string fileName, TimeSpan timeout)
        {
            ManualResetEventSlim e;
            if (this.fileLocks.TryGetValue(fileName, out e) == false)
                return;

            if (e.Wait(timeout) == false)
            {
                try
                {
                    this.DeleteFile(fileName);
                }
                finally
                {
                    this.CancelWriterLock(fileName);
                    throw new TimeoutException(ExceptionMessage.Format(
                        "Timeout ({0}) occured when waiting for avaliability of file: {1}", timeout, fileName));
                }
            }
        }

        private void ExitWriterLock(string fileName)
        {
            ManualResetEventSlim e;
            if (this.fileLocks.TryRemove(fileName, out e))
            {
                e.Set();
                e.Dispose();
            }
        }

        private void CancelWriterLock(string fileName)
        {
            ManualResetEventSlim e;
            if (this.fileLocks.TryRemove(fileName, out e))
                e.Dispose();
        }
    }
}